package br.senai.sc.controller.crud;

import java.io.Serializable;
import java.math.BigInteger;
import java.util.List;
import java.util.Set;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;

import org.jboss.seam.international.status.Messages;
import org.jboss.seam.international.status.builder.BundleKey;

import br.senai.sc.controller.business.BaciaHidrograficaBusiness;
import br.senai.sc.controller.business.CidadeBusiness;
import br.senai.sc.controller.exception.BusinessException;
import br.senai.sc.model.BaciaHidrografica;
import br.senai.sc.model.Cidade;
import br.senai.sc.model.Estado;
import br.senai.sc.util.SystemConstants;

/**
 * Contém serviços para CRUD de {@link Cidade}.
 * 
 * @author Tiago Wanke Marques &lt;tiago.marques@sc.senai.br&gt;
 */
public class CidadeCRUD implements Serializable {

	private static final long serialVersionUID = 1L;

	@PersistenceContext
	private EntityManager em;

	@Inject
	private Validator validator;

	@Inject
	private CidadeBusiness cidadeBusiness;

	@Inject
	private BaciaHidrograficaBusiness baciaBusiness;

	@Inject
	private Messages messages;

	/**
	 * Busca as cidades cadastradas.
	 * 
	 * @return Retorna as cidades encontradas.
	 */
	public List<Cidade> findCidades() {
		return em.createNamedQuery(Cidade.CIDADES, Cidade.class)
				.getResultList();
	}

	/**
	 * {@link Cidade} da {@link BaciaHidrografica} informada.
	 * 
	 * @param baciaHidrografica
	 * @throws BusinessException
	 *             Lançada caso a Bacia Hidrográfica informada não exista.
	 */
	public List<Cidade> findCidades(BaciaHidrografica baciaHidrografica)
			throws BusinessException {

		// verifica se a Bacia existe
		if (!baciaBusiness.existe(baciaHidrografica)) {
			messages.error(new BundleKey(
					SystemConstants.PATH_TO_APPLICATION_MESSAGES,
					"bacias.inexistente"));
			throw new BusinessException();
		}

		return em
				.createNamedQuery(Cidade.CIDADES_BY_BACIA, Cidade.class)
				.setParameter("idBaciaHidrografica",
						baciaHidrografica.getIdBaciaHidrografica())
				.getResultList();
	}

	/**
	 * Busca os estados cadastrados.
	 * 
	 * @return Retorna os estados encontrados.
	 */
	public List<Estado> findEstados() {
		return em.createNamedQuery(Estado.ESTADOS, Estado.class)
				.getResultList();
	}

	/**
	 * Busca uma cidade por seu ID.
	 * 
	 * @param idCidade
	 * @return {@link Cidade} encontrada ou nulo.
	 */
	public Cidade findCidade(Integer idCidade) {

		if (idCidade == null) {
			throw new IllegalArgumentException(
					"Passado identificador null para procurar Cidade.");
		}

		return em.find(Cidade.class, idCidade);
	}

	/**
	 * Busca um estado por seu ID.
	 * 
	 * @param idEstado
	 *            ID do estado à ser buscado.
	 * @return Estado encontrado ou nulo.
	 */
	public Estado findEstado(Integer idEstado) {
		return em.find(Estado.class, idEstado);
	}

	/**
	 * @return Quantidade de {@link Cidade} cadastradas.
	 */
	public int totalCidades() {
		BigInteger total = (BigInteger) em.createNativeQuery(
				"SELECT count(*) FROM cidade c").getSingleResult();

		return total.intValue();
	}

	/**
	 * Converte a lista de {@link ConstraintViolation} para uma lista de
	 * {@link BundleKey}.
	 * 
	 * @param constraints
	 * @return
	 */
	private void toBundleKeyList(Set<ConstraintViolation<Cidade>> constraints) {

		for (ConstraintViolation<Cidade> constraint : constraints) {
			messages.error(new BundleKey(
					SystemConstants.PATH_TO_COMMON_MESSAGES, constraint
							.getMessageTemplate()));
		}
	}

	/**
	 * Persiste os dados de uma cidade. Caso esteja salvando uma nova
	 * {@link Cidade} valida as seguintes regras: - Não pode existir cidade com
	 * mesmo nome no banco - O Estado informado deve existir no banco
	 * 
	 * Caso esteja editando uma {@link Cidade} valida as seguintes regras: -
	 * Cidade existe - Estado existe - Não pode haver cidade com mesmo nome no
	 * banco
	 * 
	 * @param cidade
	 *            Cidade que terá os dados persistidos.
	 * @return Retorna a cidade persistida.
	 * @throws BusinessException
	 *             Lançada caso houver alguma validação de negócio.
	 */
	public Cidade salvar(Cidade cidade) throws BusinessException {

		if (cidade == null) {
			throw new IllegalArgumentException(
					"Tentando salvar cidade informando null.");
		}

		Set<ConstraintViolation<Cidade>> constraints = validator
				.validate(cidade);
		if (constraints.size() > 0) {
			toBundleKeyList(constraints);
			throw new BusinessException();
		}

		if (cidade.getIdCidade() == null) {
			inserir(cidade);
			return cidade;
		}

		return atualizar(cidade);
	}

	/**
	 * Efetua as regras de negócio para salvar uma nova {@link Cidade}.
	 * 
	 * @param cidade
	 * @throws BusinessException
	 */
	private void inserir(Cidade cidade) throws BusinessException {

		if (!cidadeBusiness.podeSalvarCidade(cidade)) {
			throw new BusinessException();
		}
		em.persist(cidade);
	}

	/**
	 * Efetua as regras de negócio para atualiza {@link Cidade}.
	 * 
	 * @param cidade
	 * @return {@link Cidade} atualizada.
	 * @throws BusinessException
	 */
	private Cidade atualizar(Cidade cidade) throws BusinessException {

		if (!cidadeBusiness.podeEditarCidade(cidade)) {
			throw new BusinessException();
		}
		return em.merge(cidade);

	}

	/**
	 * Remove uma cidade do banco de dados. Este método efetua um merge na
	 * entidade antes de remove-la para garantir que a mesma esteja attached.
	 * 
	 * @param cidade
	 *            Cidade que será removida.
	 * @throws BusinessException
	 *             Lançada caso houver alguma validação que não seja atendida.
	 */
	public void removerCidade(Cidade cidade) throws BusinessException {

		// a verificação de null não é feita aqui, pois o metodo cidadeExiste já
		// garante esta verificação

		if (!cidadeBusiness.podeRemoverCidade(cidade)) {
			throw new BusinessException();
		}

		em.remove(em.merge(cidade)); // garante que a entidade seja attached
	}
}
